package com.donger.auth;

import com.donger.common.core.constant.SecurityConstants;
import com.donger.common.security.service.BootClientDetailsService;
import com.donger.common.security.service.BootUserDetailsService;
import com.donger.common.security.service.BootWebResponseExceptionTranslator;
import com.donger.common.security.service.UserDetail;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 认证服务器配置(授权服务配置)
 * 使用 @EnableAuthorizationServer 来配置授权服务机制，并继承 AuthorizationServerConfigurerAdapter 该类重写 configure 方法定义授权服务器策略
 *
 * AuthorizationServerConfigurerAdapter中有三个configure
 *
 *    <1> 配置AuthorizationServer安全认证的相关信息，创建ClientCredentialsTokenEndpointFilter核心过滤器
 *    @Override
 *        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
 *    }
 *
 *    <2> 配置OAuth2的客户端相关信息
 *    @Override
 *    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
 *    }
 *
 *    <3> 配置AuthorizationServerEndpointsConfigurer众多相关类，包括配置身份认证器，配置认证方式，TokenStore，TokenGranter，OAuth2RequestFactory
 *    @Override
 *    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
 *    }
 *
 * @author xyx
 */
@Configuration
@Order(Integer.MIN_VALUE)
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    //DataSource可以看作数据源，它封装了数据库参数，连接数据库，程序中操作DataSource对象即可对数据库进行增删改查操作。
    private final DataSource dataSource;
    // 授权类型为 password 注入一个默认实现 AuthenticationManager 方法
    private final AuthenticationManager authenticationManager;
    // 自定义的 BootUserDetailsService 继承了 UserDetailService
    private final BootUserDetailsService userDetailsService;
    private final RedisConnectionFactory redisConnectionFactory;

    /**
     *  定义存储令牌方式 （以下基于JDBC）
     *  1. InMemory 基于内存存储令牌
     *  2. JDBC 基于JDBC存储令牌
     *  3. 基于JWT存储令牌
     *
     * 用来配置客户端详情服务（ClientDetailsService），
     * 客户端详情信息在这里进行初始化，你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
     * ClientDetailsServiceConfigurer 能够使用内存或 JDBC 方式实现获取已注册的客户端详情
     *
     *  jdbc 模式存储令牌
     * 使用数据库存储，只要提供这些接口的实现类即可，框架已经为我们写好 JdbcTokenStore 和 JdbcClientDetailsService
     * BootClientDetailsService 继承了 JdbcClientDetailsService 可以重写方法
     *
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        BootClientDetailsService clientDetailsService = new BootClientDetailsService(dataSource);
        clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
        clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
        clients.withClientDetails(clientDetailsService);
    }

    /**
     * 配置授权（authorization）的属性 和 增强功能
     *
     * 用来配置授权（authorization）以及令牌（token）的访问端点和令牌服务(token services)。
     * 配置AuthorizationServerEndpointsConfigurer众多相关类，包括配置身份认证器，配置认证方式
     *
     * 此配置授权类型为 Grant Types
     * 授权是使用 AuthorizationEndpoint 这个端点来进行控制的，使用 AuthorizationServerEndpointsConfigurer 这个对象实例来进行配置，
     * 默认是支持除了密码授权外所有标准授权类型，它可配置以下属性：
     * 1. authenticationManager：认证管理器，当你选择了资源所有者密码（password）授权类型的时候，请设置这个属性注入一个 AuthenticationManager 对象
     * 2. userDetailsService：可定义自己的 UserDetailsService 接口实现
     * *3. authorizationCodeServices：用来设置收取码服务的（即 AuthorizationCodeServices 的实例对象），主要用于 "authorization_code" 授权码类型模式
     * *4. implicitGrantService：这个属性用于设置隐式授权模式，用来管理隐式授权模式的状态
     * *5. tokenGranter：完全自定义授权服务实现（TokenGranter 接口实现），只有当标准的四种授权模式已无法满足需求时
     *
     * @param endpoints
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                // token存储方式
                .tokenStore(tokenStore())
                // 自定义/添加 token存储信息
                .tokenEnhancer(tokenEnhancer())
                // 认证管理器
                .authenticationManager(authenticationManager)
                .reuseRefreshTokens(false)
                //可定义自己的 UserDetailsService 接口实现
                .userDetailsService(userDetailsService)
                .exceptionTranslator(new BootWebResponseExceptionTranslator());
    }

    /**
     * 主要是让/oauth/token支持client_id以及client_secret作登录认证
     *
     * AuthorizationServerSecurityConfigurer，这个相当于pre认证，而AuthorizationServerEndpointsConfigurer这个相当于after认证。
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                // 允许客户端的表单身份验证
                .allowFormAuthenticationForClients()
                // 经过身份验证
                .tokenKeyAccess("isAuthenticated()")
                // 全部允许
                .checkTokenAccess("permitAll()");
    }

    /**
     * token存储方式
     *
     * 声明TokenStore实现
     * TokenStore 的默认实现有三种：
     *
     * InMemoryTokenStore : 存储在内存中
     * JdbcTokenStore : 这个是基于JDBC的实现，令牌（Access Token）会保存到数据库。这个方式，可以在多个服务之间实现令牌共享。
     * JwtTokenStore : jwt全称 JSON Web Token。这个实现方式不用管如何进行存储（内存或磁盘），因为它可以把相关信息数据编码存放在令牌里。
     *
     * RedisTokenStore : 由于TokenStore作用就是对于OAuth2令牌持久化接口，而我们在实际开发中，对于内存的使用是慎之又慎，
     *                  而对于存储到数据库也是根据项目需求进行调配。因此就想，可不可以用redis来进行存储持久化我们的OAuth2令牌。
     *                  偷偷瞄了一眼OAuth2还有那些实现了TokenStore的，找到了一个RedisTokenStore。
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * 自定义/添加 token存储信息
     *
     * 有时候需要额外的信息加到token返回中，这部分也可以自定义，此时我们可以自定义一个TokenEnhancer
     * 存放额外信息（这部分信息不关乎加密方式）
     *
     * TokenEnhancer 接口提供一个 enhance(OAuth2AccessToken var1, OAuth2Authentication var2) 方法，用于对token信息的添加
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            if (SecurityConstants.CLIENT_CREDENTIALS
                    .equals(authentication.getOAuth2Request().getGrantType())) {
                return accessToken;
            }

            final Map<String, Object> additionalInfo = new HashMap<>(8);
            UserDetail userDetail = (UserDetail) authentication.getUserAuthentication().getPrincipal();
            additionalInfo.put(SecurityConstants.DETAILS_USER_ID, userDetail.getUserId());
            additionalInfo.put(SecurityConstants.DETAILS_USERNAME, userDetail.getUsername());
            additionalInfo.put(SecurityConstants.DETAILS_DEPT_ID, userDetail.getDeptId());
            additionalInfo.put(SecurityConstants.DETAILS_TENANT_ID,userDetail.getTenantId());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            return accessToken;
        };
    }
}
